/*
 * Copyright 2015 Realm Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.realm.internal;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

import io.realm.RealmFieldType;


/**
 * The Subclasses of this are a fast cache of column indices, for proxy object.
 * <p>
 * The fast cache functionality is implemented in the Proxy classes generated by {@code RealmProxyClassGenerator}.
 * Every proxy object will hold an reference of its {@code ColumnInfo}. The ref to the {@code ColumnInfo} instance is
 * maintained by the {@link ColumnIndices}. As long as the reference in proxy object has been set, it should never be
 * changed. When schema changes, the relevant {@code ColumnInfo} instance's content will be refreshed therefore the
 * proxy object could have the latest column indices. Be sure to understand what is going on there,
 * before changing things here.
 * <p>
 * While the use of the fields in {@code ColumnDetails} is consistent, there are three subtly different cases:
 * <ul>
 * <li>If the column type is a simple type, the {@code linkedClassName} field is {@code null}</li>
 * <li>If the column type is OBJECT or LINK, the {@code linkedClassName} field is the class name of the OBJECT/LINK type</li>
 * <li>If the column type is LINKING_OBJECT, the {@code linkedClassName} field is the class name of the backlink source table
 * and the column index field is the index of the backlink source field, in the source table</li>
 * </ul>
 * <p>
 * The instance of this class is dedicated to a single {@link OsSharedRealm} instance. Thus this is not supposed to be
 * used across threads.
 * An instance can be mutated, after construction, in four ways:
 * <ul>
 * <li>the {@code copyFrom} method</li>
 * <li>as the dst parameter of the two-argument copy method</li>
 * <li>using the {@code addColumnDetails} method</li>
 * <li>using the {@code addBacklinkDetails} method</li>
 * </ul>
 * Immutable instances of this class protect against the first possibility by throwing on calls
 * to {@code copyFrom}.  There are no checks against the other three mutations.  In order to comply
 * with the effectively-final contract:
 * <ul>
 * <li>the methods {@code addColumnDetails} and {@code addBacklinkDetails} must be called
 * only from within instance constructors</li>
 * <li>an immutable instance must never be the dst parameter of the two-argument copy method</li>
 * </ul>
 */
public abstract class ColumnInfo {

    // Immutable column information
    public static final class ColumnDetails {
        public final long columnKey;
        public final RealmFieldType columnType;
        public final String linkedClassName;

        private ColumnDetails(long columnKey, RealmFieldType columnType, @Nullable String linkedClassName) {
            // invariant: (columnType == OBJECT || columnType == LIST || columnType == LINKING_OBJECTS) == (linkedClassName != null)
            this.columnKey = columnKey;
            this.columnType = columnType;
            this.linkedClassName = linkedClassName;
        }

        ColumnDetails(Property property) {
            this(property.getColumnKey(), property.getType(), property.getLinkedObjectName());
        }

        @Override
        public String toString() {
            StringBuilder buf = new StringBuilder("ColumnDetails[");
            buf.append(columnKey);
            buf.append(", ").append(columnType);
            buf.append(", ").append(linkedClassName);
            return buf.append("]").toString();
        }
    }


    private final Map<String, ColumnDetails> columnkeysFromJavaFieldNames;
    private final Map<String, ColumnDetails> columnKeysFromColumnNames;
    private final Map<String, String> javaFieldNameToInternalNames;
    private final boolean mutable;

    /**
     * Create a new, empty instance
     *
     * @param mapSize the expected number of columns in the map.
     */
    protected ColumnInfo(int mapSize) {
        this(mapSize, true);
    }

    /**
     * Create an exact copy of the passed instance.
     *
     * @param src the instance to copy
     * @param mutable false to make this instance effectively final
     */
    protected ColumnInfo(@Nullable ColumnInfo src, boolean mutable) {
        this((src == null) ? 0 : src.columnkeysFromJavaFieldNames.size(), mutable);
        // ColumnDetails are immutable and may be re-used.
        if (src != null) {
            columnkeysFromJavaFieldNames.putAll(src.columnkeysFromJavaFieldNames);
        }
    }

    private ColumnInfo(int mapSize, boolean mutable) {
        this.columnkeysFromJavaFieldNames = new HashMap<>(mapSize);
        this.columnKeysFromColumnNames = new HashMap<>(mapSize);
        this.javaFieldNameToInternalNames = new HashMap<>(mapSize);
        this.mutable = mutable;
    }

    /**
     * Get the mutability state of the instance.
     *
     * @return true if the instance is mutable
     */
    public final boolean isMutable() {
        return mutable;
    }

    /**
     * Returns the column key, in the described table, for the named column.
     *
     * @return column key.
     */
    public long getColumnKey(String javaFieldName) {
        ColumnDetails details = columnkeysFromJavaFieldNames.get(javaFieldName);
        return (details == null) ? -1 : details.columnKey;
    }

    /**
     * Returns the {@link ColumnDetails}, in the described table, of the named column.
     *
     * @return {@link ColumnDetails} or {@code null} if not found.
     */
    @Nullable
    public ColumnDetails getColumnDetails(String javaFieldName) {
        return columnkeysFromJavaFieldNames.get(javaFieldName);
    }

    /**
     * Returns the internal field name that corresponds to the name found in the Java model class.
     * @param javaFieldName the field name in the Java model class.
     * @return the internal field name or {@code null} if the java name doesn't exists.
     */
    @Nullable
    public String getInternalFieldName(String javaFieldName) {
        return javaFieldNameToInternalNames.get(javaFieldName);
    }

    /**
     * Makes this ColumnInfo an exact copy of {@code src}.
     *
     * @param src The source for the copy.  This instance will be an exact copy of {@code src} after return.
     * {@code src} must not be {@code null}.
     * @throws IllegalArgumentException if {@code other} has different class than this.
     */
    public void copyFrom(ColumnInfo src) {
        if (!mutable) {
            throw new UnsupportedOperationException("Attempt to modify an immutable ColumnInfo");
        }
        if (null == src) {
            throw new NullPointerException("Attempt to copy null ColumnInfo");
        }

        columnkeysFromJavaFieldNames.clear();
        columnkeysFromJavaFieldNames.putAll(src.columnkeysFromJavaFieldNames);
        columnKeysFromColumnNames.clear();
        columnKeysFromColumnNames.putAll(src.columnKeysFromColumnNames);
        javaFieldNameToInternalNames.clear();
        javaFieldNameToInternalNames.putAll(src.javaFieldNameToInternalNames);
        copy(src, this);
    }

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder("ColumnInfo[");
        buf.append("mutable="+mutable).append(",");
        if (columnkeysFromJavaFieldNames != null) {
            buf.append("JavaFieldNames=[");
            boolean commaNeeded = false;
            for (Map.Entry<String, ColumnDetails> entry : columnkeysFromJavaFieldNames.entrySet()) {
                if (commaNeeded) { buf.append(","); }
                buf.append(entry.getKey()).append("->").append(entry.getValue());
                commaNeeded = true;
            }
            buf.append("]");
        }
        if (columnKeysFromColumnNames != null) {
            buf.append(", InternalFieldNames=[");
            boolean commaNeeded = false;
            for (Map.Entry<String, ColumnDetails> entry : columnKeysFromColumnNames.entrySet()) {
                if (commaNeeded) { buf.append(","); }
                buf.append(entry.getKey()).append("->").append(entry.getValue());
                commaNeeded = true;
            }
            buf.append("]");
        }
        return buf.append("]").toString();
    }

    /**
     * Create a new object that is an exact copy of {@code src}.
     * This is the generic factory for ColumnInfo objects.
     * Subclasses are expected to override it with a proxy to a copy constructor.
     *
     * @param mutable false to make an immutable copy.
     */
    protected abstract ColumnInfo copy(boolean mutable);

    /**
     * Make {@code dst} into an exact copy of {@code src}.
     * Intended for use only by subclasses.
     * NOTE: there is no protection against calling this method with an "immutable" instance as dst!
     *
     * @param src The source for the copy
     * @param dst The destination of the copy.  Will be an exact copy of src after return.
     */
    protected abstract void copy(ColumnInfo src, ColumnInfo dst);

    /**
     * Add a new column to the indexMap.
     * <p>
     * <b>For use only in subclass constructors!</b>.
     * Must be called from within the subclass constructor, to maintain the effectively-final contract.
     * <p>
     * No validation done here.  Presuming that all necessary validation takes place in {@code Proxy.validateTable}.
     *
     * @param javaFieldName The name of the java field name.
     * @param internalColumnName The underlying column name in the Realm file for the Java field name.
     * @param objectSchemaInfo the {@link OsObjectSchemaInfo} for the corresponding {@code RealmObject}.
     * @return the index of the column in the table.
     */
    protected final long addColumnDetails(String javaFieldName, String internalColumnName, OsObjectSchemaInfo objectSchemaInfo) {
        Property property = objectSchemaInfo.getProperty(internalColumnName);
        ColumnDetails cd = new ColumnDetails(property);
        columnkeysFromJavaFieldNames.put(javaFieldName, cd);
        columnKeysFromColumnNames.put(internalColumnName, cd);
        javaFieldNameToInternalNames.put(javaFieldName, internalColumnName);
        return property.getColumnKey();
    }

    /**
     * Add a new backlink to the indexMap.
     * <b>For use only by subclasses!</b>.
     * Must be called from within the subclass constructor, to maintain the effectively-final contract.
     *
     * @param schemaInfo the {@link OsSchemaInfo} of the corresponding {@code Realm} instance.
     * @param javaFieldName The name of the backlink column.
     * @param sourceTableName The name of the backlink source class.
     * @param sourceJavaFieldName The name of the backlink source field.
     */
    protected final void addBacklinkDetails(OsSchemaInfo schemaInfo, String javaFieldName, String sourceTableName, String sourceJavaFieldName) {
        long columnKey = schemaInfo.getObjectSchemaInfo(sourceTableName).getProperty(sourceJavaFieldName).getColumnKey();
        columnkeysFromJavaFieldNames.put(javaFieldName, new ColumnDetails(columnKey, RealmFieldType.LINKING_OBJECTS, sourceTableName));
    }

    /**
     * Returns the {@link Map} that is the implementation for this object.
     * <b>FOR TESTING USE ONLY!</b>
     *
     * @return the column details map.
     */
    @SuppressWarnings("ReturnOfCollectionOrArrayField")
    public Map<String, ColumnDetails> getColumnKeysMap() {
        return columnkeysFromJavaFieldNames;
    }
}
